/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.business;

import TimeRecording.business.calcs.DailyAllocation;
import TimeRecording.enums.PartOfDay;
import TimeRecording.tables.AllocationOfHours;
import TimeRecording.tables.Control;
import TimeRecording.tables.Employee;
import TimeRecording.tables.HolidayRequest;
import TimeRecording.tables.MonthlyControl;
import TimeRecording.tables.SicknessReport;
import TimeRecording.tables.WeeklyTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import javax.persistence.EntityManager;

/**
 * This class is the focal point for the costing of holiday requests and
 * sickness reports and the allocation of working time to specific working
 * patterns plus holiday and sickness discounting.
 *
 * @author david
 */
public class PeriodCalculations {

    private final static boolean WRITE_DOWN_HOLIDAYS = false;
    private final static boolean ENABLE_ESTIMATING = true;

    private DailyPeriodData periodData = new DailyPeriodData();

    /*
     * This constructor is only ever called by a subclass
     *
     */
    protected PeriodCalculations() {
    }

    /**
     * Instantiates a new "period calculator" for the given employee/date-range
     * and loads up all of the required data.
     *
     * @param entityManager
     * @param employee
     * @param dateRange
     */
    public PeriodCalculations(
            EntityManager entityManager,
            Employee employee,
            DateRange dateRange) {

        periodData.refreshData(entityManager, employee, dateRange);
    }

    /**
     * This method ensures that the data held in the class is in sync with the
     * supplied employee and data range.  As a user messes about with a holiday
     * or sickness request it will be necessary to have supporting data from
     * the database in order to calculate the spend.  Rather than going to the
     * database every time, this class attempts to cache a sensible amount of
     * data relevant (with a 2 week window around the supplied dates).
     *
     * @param entityManager
     * @param employee
     * @param firstDay
     * @param lastDay
     */
    public void refreshData(EntityManager entityManager, Employee employee,
            Date firstDay, Date lastDay) {

        // If it appears we already have the required data (same employee and
        // dates are still within range) then there's no need to refresh

        if (periodData.refreshRequired(employee, firstDay, lastDay)) {

            // Expand the window by two weeks either side
            // to avoid unnecessary database access

            DateRange dateRange = new DateRange(firstDay, lastDay);
            dateRange.expand(GregorianCalendar.WEEK_OF_YEAR, 2);

            periodData.refreshData(entityManager, employee, dateRange);
        }

        reassessWorkingDays(entityManager);
    }

    /**
     * This method adds/replaces the supplied holiday request in the underlying
     * collection of holidays.
     *
     * @param localHolidayRequest
     */
    public void insertHolidayRequest(HolidayRequest localHolidayRequest) {
        periodData.insertHolidayRequest(localHolidayRequest);
    }

    /**
     * This method adds/replaces the supplied sickness report in the underlying
     * collection of sickness reports.
     *
     * @param localSicknessReport
     */
    public void insertSicknessReport(SicknessReport localSicknessReport) {
        periodData.insertSicknessReport(localSicknessReport);
    }

    public ArrayList<DailyAllocation> getDailyInformation() {
        return periodData.getDailyInformation();
    }

    /**
     * This method will re-assess the patterns, sickness, holidays, hours
     * worked, etc. for all employees and flash any changes made back to
     * the database.
     *
     * @param entityManager
     * @param dateRange
     */
    public static void reassessAndPersistWorkingDays(EntityManager entityManager,
            DateRange dateRange) {

        List<Employee> employees = Employee.getActiveEmployees(entityManager);

        for (Employee employee : employees) {
            reassessAndPersistWorkingDays(entityManager, employee, dateRange);
        }
    }

    /**
     * This method will re-assess the patterns, sickness, holidays, hours
     * worked, etc. for the specific employee and flash any changes made back to
     * the database.
     *
     * @param entityManager
     * @param employee
     * @param dateRange
     */
    public static void reassessAndPersistWorkingDays(EntityManager entityManager,
            Employee employee, DateRange dateRange) {

        if (dateRange == null) {
            dateRange = Control.getActiveWindow(entityManager);
        }

        PeriodCalculations calculations = new PeriodCalculations(
                entityManager, employee, dateRange);

        ArrayList<WeeklyTime> modifiedTimes =
                calculations.reassessWorkingDays(entityManager);

        for (WeeklyTime weeklyTimeRecord : modifiedTimes) {
            entityManager.persist(weeklyTimeRecord);
        }
    }

    /**
     * This method will re-assess the patterns, sickness, holidays, hours
     * worked, etc. for the the current date-range and employee details.
     *
     * Note that nothing is written back to the database.
     *
     * @param entityManager
     * @return
     */
    protected ArrayList<WeeklyTime> reassessWorkingDays(EntityManager entityManager) {

        // Ensure that the underlying data is consistent
        periodData.processData();

        DateRange periodRange = null;
        MonthlyControl currentMonth = MonthlyControl.getLatest(entityManager);

        if (currentMonth != null) {
            periodRange = new DateRange(
                    currentMonth.getStartDate(),
                    currentMonth.getEndDate());
        }

        ArrayList<WeeklyTime> modifiedWeeklyTimes = new ArrayList<WeeklyTime>();

        // Loop through each week of the period
        for (Iterator<DailyAllocation> day = 
                periodData.getDailyInformation().iterator(); day.hasNext();) {

            DailyAllocation monday = day.next();
            DailyAllocation tuesday = day.next();
            DailyAllocation wednesday = day.next();
            DailyAllocation thursday = day.next();
            DailyAllocation friday = day.next();
            DailyAllocation saturday = day.next();
            DailyAllocation sunday = day.next();

            // Smooth out the expected times for the week
            smoothPatterns(monday, tuesday, wednesday, thursday, friday);

            if (ENABLE_ESTIMATING && periodRange != null) {
                monday.recalculateEstimated(periodRange);
                tuesday.recalculateEstimated(periodRange);
                wednesday.recalculateEstimated(periodRange);
                thursday.recalculateEstimated(periodRange);
                friday.recalculateEstimated(periodRange);
                saturday.recalculateEstimated(periodRange);
                sunday.recalculateEstimated(periodRange);
            }

            // Initially try to satisfy all fixed time that's expected
            satisfyFixedHours(monday, tuesday, wednesday, thursday,
                    friday, saturday, sunday);

            // Now satisfy any sessional work that's expected
            satisfySessionalHours(monday, tuesday, wednesday, thursday,
                    friday, saturday, sunday);

            // Try to write-down holidays (only doing so where we are not
            // compromising any semi-flexi commitments for the week)

            if (WRITE_DOWN_HOLIDAYS) {
                writeDownHolidays(monday, tuesday, wednesday, thursday,
                        friday, saturday, sunday);
            }

            // Allocate any remaining hours primarily towards semi-flexi (aka
            // weekly flexi) or else full flexi.
            allocateSemiFlexiAndFlexiHours(monday, tuesday, wednesday,
                    thursday, friday, saturday, sunday);

            // Provide accurate costings for approved holidays
            calculateHolidaySpend(monday, tuesday, wednesday, thursday,
                    friday, saturday, sunday);

            // Provide indicative costings for unapproved holidays
            calculateUnapprovedHolidaySpend(monday, tuesday, wednesday,
                    thursday, friday, saturday, sunday);

            // Provide accurate costings for sick leave
            calculateSicknessSpend(monday, tuesday, wednesday, thursday,
                    friday, saturday, sunday);

           // Provide indicative costings for unapproved sick leave
            calculateUnapprovedSicknessSpend(monday, tuesday, wednesday, thursday,
                    friday, saturday, sunday);

            // We need to recalculate the estimates at this point as the
            // values we calculated earlier will have been shared amongst
            // the various patterns and therefore consumed.
            
            if (ENABLE_ESTIMATING && periodRange != null) {
                monday.recalculateEstimated(periodRange);
                tuesday.recalculateEstimated(periodRange);
                wednesday.recalculateEstimated(periodRange);
                thursday.recalculateEstimated(periodRange);
                friday.recalculateEstimated(periodRange);
                saturday.recalculateEstimated(periodRange);
                sunday.recalculateEstimated(periodRange);
            }

            // Update the "modified weekly time" record
            // (if any changes have been made)

            GregorianCalendar startOfWeek = new GregorianCalendar();
            startOfWeek.setTime(monday.getDate());
            WeeklyTime timesheet = periodData.getWeeklyTime(startOfWeek);

            if (timesheet != null && updateTimesheet(timesheet, monday, tuesday,
                    wednesday, thursday, friday, saturday, sunday)) {

                modifiedWeeklyTimes.add(timesheet);
            }
        }

        return modifiedWeeklyTimes;
    }

    /**
     * In this method we attempt to smooth out the working week.  We accept that
     * fixed patterns of work cannot be altered but then attempt to distribute
     * semi-flexi and full-flexi expectations in a manner whereby we get the
     * flattest working profile possible.  Realistically, this smoothing only
     * really affects holidays and sickness as clocked hours in excess of fixed
     * commitments are allocated to semi-flexi and full-flexi in a [no pun
     * intended] flexible manner.
     *
     * @param weekdays
     */
    private void smoothPatterns(DailyAllocation... weekdays) {

        double semiFlexiPot = 0;
        double flexiPot = 0;

        // Harvest all of the expected flexi and semi-flexi
        // and put it into a pair of pots for redistribution
        for (DailyAllocation weekday : weekdays) {
            semiFlexiPot += weekday.getSemiFlexi().getExpected();
            weekday.getSemiFlexi().setExpected(0.0);
            flexiPot += weekday.getFlexi().getExpected();
            weekday.getFlexi().setExpected(0.0);
        }

        // As long as there's something left in the pot, lets
        // try to allocate in an equitables and "smooth" manner
        while (semiFlexiPot > 0.0 || flexiPot > 0.0) {

            // Find the least number of hours worked on any day of the week
            double shortestDay = Double.MAX_VALUE;
            for (DailyAllocation weekday : weekdays) {
                shortestDay = Math.min(
                        shortestDay, weekday.getTotalPatternTime());
            }

            // Find the next least number of hours worked on any day of the week
            // and the total number of days that meet the "short day" criteria
            double nextShortestDay = Double.MAX_VALUE;
            int noOfShortDays = 0;
            for (DailyAllocation weekday : weekdays) {
                if (weekday.getTotalPatternTime() == shortestDay) {
                    noOfShortDays++;
                } else {
                    nextShortestDay = Math.min(
                            nextShortestDay, weekday.getTotalPatternTime());
                }
            }

            double semiFlexiAllocatedPerDay = 0.0;
            double flexiAllocatedPerDay = 0.0;

            // Are all five days already nice and smooth?
            if (noOfShortDays == 5) {

                semiFlexiAllocatedPerDay = semiFlexiPot / 5;
                semiFlexiPot = 0.0;
                flexiAllocatedPerDay = flexiPot / 5;
                flexiPot = 0.0;

            } else {

                // Work out how many hours we need to drag the shortest day(s) up
                // to the same number of hours as the next shortest working day

                double hoursRequired = nextShortestDay - shortestDay;
                double totalHoursRequired = hoursRequired * noOfShortDays;

                // Attempt to allocate time from one or both
                // of our pots to satisfy the shortfall


                // Anything to dish out from the semi-flexi pot?
                if (semiFlexiPot > 0.0) {

                    // More than enough in the semi-flexi pot?
                    if (semiFlexiPot > totalHoursRequired) {

                        // Give the full allocation required and reduce the pot
                        semiFlexiAllocatedPerDay = hoursRequired;
                        semiFlexiPot -= totalHoursRequired;
                        totalHoursRequired = 0;
                        hoursRequired = 0;

                    } else {

                        // Give what we can from the pot and empty it
                        semiFlexiAllocatedPerDay = semiFlexiPot / noOfShortDays;
                        totalHoursRequired -= semiFlexiPot;
                        hoursRequired -= semiFlexiAllocatedPerDay;
                        semiFlexiPot = 0;
                    }
                }

                // Anything to dish out from the flexi pot?
                if (flexiPot > 0.0) {

                    // More than enough in the semi-flexi pot?
                    if (flexiPot > totalHoursRequired) {

                        // Give the full allocation required and reduce the pot
                        flexiAllocatedPerDay = hoursRequired;
                        flexiPot -= totalHoursRequired;
                        totalHoursRequired = 0;
                        hoursRequired = 0;

                    } else {

                        // Give what we can from the pot and empty it
                        flexiAllocatedPerDay = flexiPot / noOfShortDays;
                        totalHoursRequired -= flexiPot;
                        hoursRequired -= flexiAllocatedPerDay;
                        flexiPot = 0;
                    }
                }
            }

            // Allocate the flexi and semi-flexi to the shortest days
            for (DailyAllocation weekday : weekdays) {
                if (weekday.getTotalPatternTime() == shortestDay) {
                    weekday.getSemiFlexi().adjustExpected(semiFlexiAllocatedPerDay);
                    weekday.getFlexi().adjustExpected(flexiAllocatedPerDay);
                }
            }
        }
    }

    /**
     * This method is responsible for the first-cut of allocating clocked time.
     * It simply attempts to allocate (on a day-by-day basis) as much of the pot
     * as is needed to satisfy the fixed hour expectation.  Of course, there may
     * not be enough in the pot which will result in a shortfall.
     *
     * Holidays are excepted from this particular method as the shortfall could
     * be made up by paid leave.  However, sickness is not exempted as the
     * employee would definitely be out of pocket; it's thus correct (even if a
     * sickness record has been entered) to mitigate the lost fixed hours with
     * any that have been worked.
     *
     * Note that if only half of the day has been taken as a holiday then we
     * will attempt (at this stage) to satisfy half of the expected fixed hours.
     *
     * @param days the working week (Monday to Sunday)
     */
    private void satisfyFixedHours(DailyAllocation... days) {

        for (DailyAllocation day : days) {

            // If there's nothing in the clocked pot or there are no fixed hours
            // or the whole day has been written off to holiday then we can just
            // skip this day as there's nothing we can do
            if ((day.getClocked() == 0.0 && day.getEstimated() == 0.0)||
                    day.getFixed().getExpected() == 0.0 ||
                    day.getHoliday().getPartOfDay() == PartOfDay.ALL_DAY) {
                continue;
            }

            // If we've got half a day of holiday (we've already rejected the
            // situation where we have a full day) then we half the expected
            // number of hours
            double required = day.getFixed().getExpected();
            if (day.getHoliday().getPartOfDay() != null) {
                required /= 2.0;
            }

            if (day.getClocked() > 0) {

                // Grant what we can towards the required figure and reduce the pot
                double granted = (day.getClocked() > required) ? required : day.getClocked();
                day.getFixed().adjustWorked(granted);
                day.adjustClocked(-granted);
                
            } else {

                // Grant what we can towards the required figure and reduce the pot
                double granted = (day.getEstimated() > required) ? required : day.getEstimated();
                day.getFixed().adjustWorked(granted);
                day.adjustEstimated(-granted);
            }
        }
    }

    /**
     * This method is responsible for the second-cut of allocating clocked time.
     * It simply attempts to allocate (on a day-by-day basis) as much of the pot
     * as is needed to satisfy the sessional hour expectation.  Of course, there
     * may not be enough in the pot which will result in a shortfall.
     *
     * Holidays are excepted from this particular method as the shortfall could
     * be made up by paid leave.  However, sickness is not exempted as the
     * employee would definitely be out of pocket; it's thus correct (even if a
     * sickness record has been entered) to mitigate the lost fixed hours with
     * any that have been worked.
     *
     * Note that estimated time is never allocated to sessional work; sessions
     * can only be satisfied by actual recorded time.
     *
     * Note that if only half of the day has been taken as a holiday then we
     * will attempt (at this stage) to satisfy half of the expected fixed hours.
     *
     * @param days the working week (Monday to Sunday)
     */
    private void satisfySessionalHours(DailyAllocation... days) {

        for (DailyAllocation day : days) {

            // If there's nothing in the clocked pot or there are no fixed hours
            // or the whole day has been written off to holiday then we can just
            // skip this day as there's nothing we can do
            if (day.getClocked() == 0.0 ||
                    day.getSessional().getExpected() == 0.0 ||
                    day.getHoliday().getPartOfDay() == PartOfDay.ALL_DAY) {
                continue;
            }

            // Unlike fixed hours, we make no such allowances for sessional
            // work; if the session is booked in then we expect it to be worked.
            double required = day.getSessional().getExpected();

            // Grant what we can towards the required figure and reduce the pot
            double granted = (day.getClocked() > required) ? required : day.getClocked();
            day.getSessional().adjustWorked(granted);
            day.adjustClocked(-granted);
        }
    }

    /**
     * This method will take a look at the entire week and decide how many hours
     * are required to satisfy the full-flexi and semi-flexi totals (regardless
     * of holidays or sickness) and how many currently free hours there are.  If
     * full-flexi and semi-flexi requirements can be met and there's still time
     * left in the pot then it's appropriate to write-down the cost of a holiday
     * (or holidays).
     *
     * Note: this method will only come into play in the exceptional scenario
     * when someone books a holiday but then clocks time.
     *
     * Note: An alternative approach might be to "big up" the full-flexi balance
     * but that's relying on the fact that the employee has a full-flexi pattern
     * (which isn't guaranteed).
     *
     * @param days the full week (Monday to Sunday)
     */
    private void writeDownHolidays(DailyAllocation... days) {

        // Pass 1: Work out how much time we need to keep in reserve
        // to satisfy any semi-flexi or flexi requirements for the full week
        // and how much time we have available to satisfy that requirement

        double totalFlexiRequired = 0.0;
        double totalSemiFlexiRequired = 0.0;
        double totalClocked = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getExpected();
            totalFlexiRequired += day.getFlexi().getExpected();
            totalClocked += day.getClocked();
        }

        double totalSurplus = totalClocked - totalSemiFlexiRequired - totalFlexiRequired;

        for (DailyAllocation day : days) {

            // If we've no slack for writing down holidays then just quit
            if (totalSurplus <= 0) {
                break;
            }

            // If there's nothing in the clocked pot or there's
            // no holiday for today then just skip this day
            if (day.getClocked() == 0.0 || day.getHoliday().getPartOfDay() == null) {
                continue;
            }

            // Work out any possible fixed hours shortfall we can make up
            double required = day.getFixed().getExpected() - day.getFixed().getWorked();

            // If there's nothing to be made up then skip this day
            if (required == 0.0) {
                continue;
            }

            // How much would we be able to grant from today's excess?
            double granted = (day.getClocked() > required) ? required : day.getClocked();

            // Limit the number of hours granted if necessary in order to
            // ensure we can still satisfy our semi-flexi target for the week
            granted = Math.min(granted, totalSurplus);

            day.getFixed().adjustWorked(granted);
            day.adjustClocked(-granted);
            totalSurplus -= granted;
        }
    }

    /**
     * This method's task should be straightforward.  If we can satisfy the
     * semi-flexi requirement then that number of hours should already have been
     * reserved to be allocated now.  We don't give a toss about holidays or
     * sickness.  If we've got the spare hours and we can satisfy the
     * requirements then we'll do so.
     *
     * Note that any fixed commitments have already been taken car of before
     * this method is called.
     *
     * @param days the full working week (Monday to Sunday)
     */
    private void allocateSemiFlexiAndFlexiHours(DailyAllocation... days) {

        double totalSemiFlexiRequired = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getExpected();
        }

        for (DailyAllocation day : days) {

            // As long as there's something in the pot then we can use it.
            if (day.getClocked() == 0.0 && day.getEstimated() == 0.0) {
                continue;
            }

            if (day.getClocked() > 0.0) {

                double granted = (day.getClocked() > totalSemiFlexiRequired)
                        ? totalSemiFlexiRequired : day.getClocked();

                day.getSemiFlexi().setWorked(granted);
                day.adjustClocked(-granted);
                totalSemiFlexiRequired -= granted;

                // Allocate additional hours to flexi if the employee has a
                // flexi pattern otherwise throw it in the ad hoc pot

                if (day.getClocked() > 0) {
                    if (day.hasFlexiPattern()) {
                        day.getFlexi().setWorked(day.getClocked());
                    } else {
                        day.getAdHoc().setWorked(day.getClocked());
                    }
                    day.setClocked(0.0);
                }

            } else if (day.getEstimated() > 0.0) {

                double granted = (day.getEstimated() > totalSemiFlexiRequired)
                        ? totalSemiFlexiRequired : day.getEstimated();

                day.getSemiFlexi().setWorked(granted);
                day.adjustEstimated(-granted);
                totalSemiFlexiRequired -= granted;

                if (day.getEstimated() > 0) {

                    // Allocate additional estimated hours to flexi if the
                    // employee has a flexi pattern; estimates will never
                    // go to ad hoc

                    if (day.hasFlexiPattern()) {
                        day.getFlexi().setWorked(day.getEstimated());
                    }
                    day.setEstimated(0.0);
                }
            }
        }
    }

    /**
     * In this method we will simply calculate the number of hours that were
     * expected on the given day and allocate all or half of it depending on
     * specific details of the request.  We will then allocate that pot to hours
     * against each pattern type.
     *
     * After processing, the single "holiday cost" column will be set correctly
     * for the half/full hours plus the individual pattern-centric pots will be
     * updated.
     * 
     * @param days the full week (Monday to Sunday)
     */
    private void calculateHolidaySpend(DailyAllocation... days) {

        // Work out what is still outstanding for the week

        double totalSemiFlexiRequired = 0.0;
        double totalFlexiRequired = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getPaidShortfall();
            totalFlexiRequired += day.getFlexi().getPaidShortfall();
        }
        
        for (DailyAllocation day : days) {

            // What's the maximum we're going to cost this holiday as?
            double maxSpend = day.getTotalPatternTime();
            
            if (day.getHoliday().getPartOfDay() == null) {

                // No holiday for today so just move on to the next day
                continue;

            } else {
                
                if (day.getHoliday().getPartOfDay() != PartOfDay.ALL_DAY) {
                    maxSpend *= 0.5;
                }
            }

            double holidayCost = 0.0;

            // Satisfy any fixed shortfall for this day as
            // long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = day.getFixed().getPaidShortfall();
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFixed().setHoliday(required);
                    holidayCost += required;
                    maxSpend -= required;
                }
            }

            // Satisfy any semi-flexi shortfall that we can
            // as long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = totalSemiFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getSemiFlexi().setHoliday(required);
                    holidayCost += required;
                    maxSpend -= required;
                    totalSemiFlexiRequired -= required;
                }
            }

            // Allocate any remaining holiday that will be costed for today
            // to the flexi pot.  This might mean that the employee has clocked
            // up more hours than are expected of him but so what

            if (maxSpend > 0) {
                double required = totalFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFlexi().setHoliday(maxSpend);
                    holidayCost += maxSpend;
                    maxSpend = 0;
                    totalFlexiRequired -= maxSpend;
                }
            }

            day.getHoliday().setSpend(holidayCost);
        }
    }

    /**
     * Unapproved holidays do not contribute to the weekly timesheet; these
     * calculations are purely there to provide an estimate of what the holiday
     * would cost if it were approved.  It's quite possible that the approval of
     * a holiday might actually write down the cost of a holiday; for that
     * reason, we ignore sickness when calculating the possible cost of the
     * holiday (sickness is the last resort).
     *
     * @param days the full week (Monday to Sunday)
     */
    private void calculateUnapprovedHolidaySpend(DailyAllocation... days) {

        // Work out what is still outstanding for the week

        double totalSemiFlexiRequired = 0.0;
        double totalFlexiRequired = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getPaidShortfall();
            totalFlexiRequired += day.getFlexi().getPaidShortfall();
        }

        // Work out what is still outstanding for the week

        for (DailyAllocation day : days) {

            // What's the maximum we're going to cost this holiday as?
            double maxSpend = day.getTotalPatternTime();

            if (day.getUnapprovedHoliday().getPartOfDay() == null) {

                // No holiday for today so just move on to the next day                
                continue;

            } else {

                if (day.getUnapprovedHoliday().getPartOfDay() != PartOfDay.ALL_DAY) {
                    maxSpend *= 0.5;                    
                }
            }

            double holidayCost = 0.0;

            // Satisfy any fixed shortfall for this day as
            // long as it's within our limit for the day
            if (maxSpend > 0) {               
                double required = day.getFixed().getPaidShortfall();                
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFixed().setHoliday(required);
                    holidayCost += required;
                    maxSpend -= required;
                }
            }

            // Satisfy any semi-flexi shortfall that we can
            // as long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = totalSemiFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getSemiFlexi().setHoliday(required);
                    holidayCost += required;
                    maxSpend -= required;
                    totalSemiFlexiRequired -= required;
                }
            }

            // Allocate any remaining holiday that will be costed for today
            // to the flexi pot.  This might mean that the employee has clocked
            // up more hours than are expected of him but so what

            if (maxSpend > 0) {
                double required = totalFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFlexi().setHoliday(maxSpend);
                    holidayCost += maxSpend;
                    maxSpend = 0;
                    totalFlexiRequired -= maxSpend;
                }
            }

            day.getUnapprovedHoliday().setSpend(holidayCost);
        }
    }

    /**
     * All allocation of time up to this point has made every attempt to write
     * down the cost of sickness and then holiday.  At this point, we just need
     * to look at what was scheduled in for the day (based on a smoothed working
     * schedule) and allocate as much time as we can to satisfy daily fixed
     * shortfall and then semi/full-flexi.
     *
     * Note: before this method has been called, every attempt has been made to
     * intelligently mitigate the cost of sickness and then holidays.
     *
     * e.g. We may find 8 hours expected across all patterns but only have
     * satisfied 6 of them.  However, if two of those hours were semi-flexi and
     * were taken elsewhere then we don't want to take the hit!
     *
     * @param days the full week (Monday to Sunday)
     */
    private void calculateSicknessSpend(DailyAllocation... days) {

        // Work out what is still outstanding for the week

        double totalSemiFlexiRequired = 0.0;
        double totalFlexiRequired = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getPaidShortfall();
            totalFlexiRequired += day.getFlexi().getPaidShortfall();
        }

        for (DailyAllocation day : days) {

            // What's the maximum we're going to cost this sickness as?
            double maxSpend = day.getTotalPatternTime();

            if (day.getSickness().getPartOfDay() == null) {
                // No sickness for today so just move on to the next day
                continue;
            } else {
                if (day.getSickness().getPartOfDay() != PartOfDay.ALL_DAY) {
                    maxSpend *= 0.5;
                }
            }

            double sicknessCost = 0.0;

            // Satisfy any fixed shortfall for this day as
            // long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = day.getFixed().getPaidShortfall();
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFixed().setSickness(required);
                    sicknessCost += required;
                    maxSpend -= required;
                }
            }

            // Satisfy any semi-flexi shortfall that we can
            // as long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = totalSemiFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getSemiFlexi().setSickness(required);
                    sicknessCost += required;
                    maxSpend -= required;
                    totalSemiFlexiRequired -= required;
                }
            }

            // Satisfy any flexi shortfall that we can as
            // long as it's within our limit for the day
            if (maxSpend > 0) {
                double required = totalFlexiRequired;
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFlexi().setSickness(required);
                    sicknessCost += required;
                    maxSpend -= required;
                    totalFlexiRequired -= required;
                }
            }

            day.getSickness().setSpend(sicknessCost);
        }
    }

    /**
     * Unapproved sickness do not contribute to the weekly timesheet; these
     * calculations are purely there to provide an estimate of what the sickness
     * would cost if it were approved.
     *
     * @param days the full week (Monday to Sunday)
     */
    private void calculateUnapprovedSicknessSpend(DailyAllocation... days) {

        // Work out what is still outstanding for the week

        double totalSemiFlexiRequired = 0.0;
        double totalFlexiRequired = 0.0;

        for (DailyAllocation day : days) {
            totalSemiFlexiRequired += day.getSemiFlexi().getTotalShortfall();
            totalFlexiRequired += day.getFlexi().getTotalShortfall();
        }

        for (DailyAllocation day : days) {

            // What's the maximum we're going to cost this sickness as?
            double maxSpend = day.getTotalPatternTime();

            if (day.getUnapprovedSickness().getPartOfDay() == null) {
                // No sickness for today so just move on to the next day
                continue;
            } else {
                if (day.getUnapprovedSickness().getPartOfDay() != PartOfDay.ALL_DAY) {
                    maxSpend *= 0.5;
                }
            }

            double sicknessCost = 0.0;

            // Satisfy any fixed shortfall for this day as
            // long as it's within our limit for the day
            if (maxSpend > 0) {

                double required = day.getFixed().getTotalShortfall();
                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFixed().setSickness(required);
                    sicknessCost += required;
                    maxSpend -= required;
                }
            }

            // Satisfy any semi-flexi shortfall that we can
            // as long as it's within our limit for the day
            if (maxSpend > 0) {

                double required = totalSemiFlexiRequired;

                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getSemiFlexi().setSickness(required);
                    sicknessCost += required;
                    maxSpend -= required;
                    totalSemiFlexiRequired -= required;
                }
            }

            // Satisfy any flexi shortfall that we can as
            // long as it's within our limit for the day
            if (maxSpend > 0) {

                double required = totalFlexiRequired;

                if (required > 0) {
                    required = Math.min(required, maxSpend);
                    day.getFlexi().setSickness(maxSpend);
                    sicknessCost += required;
                    maxSpend -= required;
                    totalFlexiRequired -= required;
                }
            }

            day.getUnapprovedSickness().setSpend(sicknessCost);
        }
    }

    /**
     * This method will update the specified timesheet with details from each of
     * the individual days.  The method returns a boolean indicating whether or
     * not any actual changes have been made.
     *
     * @param timesheet
     * @param monday
     * @param tuesday
     * @param wednesday
     * @param thursday
     * @param friday
     * @param saturday
     * @param sunday
     * @return indicates whether or not the timesheet has changed
     */
    private boolean updateTimesheet(
            WeeklyTime timesheet, DailyAllocation monday,
            DailyAllocation tuesday, DailyAllocation wednesday,
            DailyAllocation thursday, DailyAllocation friday,
            DailyAllocation saturday, DailyAllocation sunday) {

        boolean changeMade = false;

        if (updateTimeSheetForDay(monday, timesheet.getMonday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(tuesday, timesheet.getTuesday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(wednesday, timesheet.getWednesday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(thursday, timesheet.getThursday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(friday, timesheet.getFriday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(saturday, timesheet.getSaturday())) {
            changeMade = true;
        }

        if (updateTimeSheetForDay(sunday, timesheet.getSunday())) {
            changeMade = true;
        }
        
        return changeMade;
    }

    /**
     * This method transfers data from a set of calculated daily totals into a
     * copy for persistence.  A boolean is returned indicating whether or not
     * any actual changes have been made.
     *
     * @param day
     * @param timesheetDay
     * @return indicates whether or not the "day to be persisted" has changed
     */
    private boolean updateTimeSheetForDay(DailyAllocation day, 
            AllocationOfHours timesheetDay) {

        boolean changeMade = false;

        // Update the estimate if there is one
        
        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getEstimated(), day.getEstimated())) {

            timesheetDay.setEstimated(day.getEstimated());
            changeMade = true;
        }

        // Update the fixed details first

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFixedExpected(), day.getFixed().getExpected())) {

            timesheetDay.setFixedExpected(day.getFixed().getExpected());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFixedRecorded(), day.getFixed().getWorked())) {

            timesheetDay.setFixedRecorded(day.getFixed().getWorked());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFixedSickness(), day.getFixed().getSickness())) {

            timesheetDay.setFixedSickness(day.getFixed().getSickness());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFixedHoliday(), day.getFixed().getHoliday())) {

            timesheetDay.setFixedHoliday(day.getFixed().getHoliday());
            changeMade = true;
        }

        // Update the semi-flexi details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSemiFlexiExpected(), day.getSemiFlexi().getExpected())) {

            timesheetDay.setSemiFlexiExpected(day.getSemiFlexi().getExpected());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSemiFlexiRecorded(), day.getSemiFlexi().getPaid())) {

            timesheetDay.setSemiFlexiRecorded(day.getSemiFlexi().getPaid());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSemiFlexiSickness(), day.getSemiFlexi().getSickness())) {

            timesheetDay.setSemiFlexiSickness(day.getSemiFlexi().getSickness());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSemiFlexiHoliday(), day.getSemiFlexi().getHoliday())) {

            timesheetDay.setSemiFlexiHoliday(day.getSemiFlexi().getHoliday());
            changeMade = true;
        }

        // Update the flexi details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFlexiExpected(), day.getFlexi().getExpected())) {

            timesheetDay.setFlexiExpected(day.getFlexi().getExpected());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFlexiRecorded(), day.getFlexi().getPaid())) {

            timesheetDay.setFlexiRecorded(day.getFlexi().getPaid());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFlexiSickness(), day.getFlexi().getSickness())) {

            timesheetDay.setFlexiSickness(day.getFlexi().getSickness());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getFlexiHoliday(), day.getFlexi().getHoliday())) {

            timesheetDay.setFlexiHoliday(day.getFlexi().getHoliday());
            changeMade = true;
        }

        // Update the sessional details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSessionExpected(), day.getSessional().getExpected())) {

            timesheetDay.setSessionExpected(day.getSessional().getExpected());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSessionRecorded(), day.getSessional().getPaid())) {

            timesheetDay.setSessionRecorded(day.getSessional().getPaid());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSessionSickness(), day.getSessional().getSickness())) {

            timesheetDay.setSessionSickness(day.getSessional().getSickness());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSessionHoliday(), day.getSessional().getHoliday())) {

            timesheetDay.setSessionHoliday(day.getSessional().getHoliday());
            changeMade = true;
        }

        // Update the ad hoc details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getAdHocRecorded(), day.getAdHoc().getPaid())) {

            timesheetDay.setAdHocRecorded(day.getAdHoc().getPaid());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getAdHocSickness(), day.getAdHoc().getSickness())) {

            timesheetDay.setAdHocSickness(day.getAdHoc().getSickness());
            changeMade = true;
        }

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getAdHocHoliday(), day.getAdHoc().getHoliday())) {

            timesheetDay.setAdHocHoliday(day.getAdHoc().getHoliday());
            changeMade = true;
        }

        // Update the holiday details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getHoliday(), day.getHoliday().getSpend())) {

            timesheetDay.setHoliday(day.getHoliday().getSpend());
            changeMade = true;
        }

        // Update the sickness details

        if (!DaisyChainUtilities.nullAwareDoublesEqual(
                timesheetDay.getSickness(), day.getSickness().getSpend())) {

            timesheetDay.setSickness(day.getSickness().getSpend());
            changeMade = true;
        }

        return changeMade;
    }

    public AllocationOfHours getTotals() {
        return periodData.getTotals();
    }
}